/*    
 * Copyright (C) 2006  ankostis, Stefan Kuper

 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package net.googlecode.weblogic_jaxb1codec;

import java.net.URL;
import java.util.Iterator;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.Validator;
import javax.xml.bind.util.ValidationEventCollector;
import javax.xml.soap.MimeHeader;
import javax.xml.transform.stream.StreamSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import weblogic.webservice.encoding.AbstractCodec;
import weblogic.xml.schema.binding.DeserializationContext;
import weblogic.xml.schema.binding.DeserializationException;
import weblogic.xml.schema.binding.SerializationContext;
import weblogic.xml.schema.binding.SerializationException;
import weblogic.xml.stream.Attribute;
import weblogic.xml.stream.XMLInputStream;
import weblogic.xml.stream.XMLName;
import weblogic.xml.stream.XMLOutputStream;

import com.sun.xml.bind.marshaller.CharacterEscapeHandler;
import com.sun.xml.bind.marshaller.NamespacePrefixMapper;

/**
 * Uses the weblogic's streaming API to construct strings that are fed to JAXB (un)marshaller 
 * for performing the (de)serialization of non-built-in types.
 * 
 * <p>
 * It is not tested with i18n encodings.
 * 
 * @author ankostis, SK
 */
public abstract class JaxbCodec extends AbstractCodec {

    abstract protected JAXBContext createJaxbContext();

    private static Logger classLogger = LoggerFactory.getLogger(JaxbCodec.class);

    private static JAXBContext jaxbContext;
    

    protected Logger getLogger() {
        return classLogger;
    }
    
    /**
     * Defines whether to validate input on de-serialization (unmarshalling).
     *  
     * @return false
     */
    protected boolean isInputValidated() {
       return false; 
    }

    /**
     * Defines whether to validate input on de-serialization (unmarshalling).
     *  
     * @return false
     */
    protected boolean isOutputValidated() {
       return false; 
    }

    /**
     * Defines whether to format output on serialization (marshalling).
     *  
     * @return false
     */
    protected boolean isOutputFormated() {
       return false; 
    }

    /**
     * Defines the URL of the referenced schema of the serialized (marshalled) document, 
     * or null for not using the 'xsi:schemaLocation' attribute.
     * Mutually exclusive with getOutputNoNamespaceSchemaLocation().
     * 
     * @return null
     * @see javax.xml.bind.Marshaller#JAXB_SCHEMA_LOCATION
     */
    protected String getOutputSchemaLocation() {
        return null;
    }

    /**
     * Defines the URL of the referenced schema of the serialized (marshalled) document.
     * or null for not using the 'xsi:noNamespaceSchemaLocation' attribute.
     * Mutually exclusive with getOutputSchemaLocation().
     * 
     * @return null 
     * @see javax.xml.bind.Marshaller#JAXB_NO_NAMESPACE_SCHEMA_LOCATION
     */
    protected String getOutputNoNamespaceSchemaLocation() {
        return null;
    }

    /**
     * Defines the escape handler of the serialized (marshalled) document.
     * or null for not using any.
     * 
     * @return null 
     */
    protected CharacterEscapeHandler getOutputCharacterEscapeHandler() {
        return null;
    }
    
    /**
     * Defines the Namespace Prefix Mapper of the serialized (marshalled) document.
     * or null for not using any.
     * 
     * @return null 
     */
    protected NamespacePrefixMapper getOutputNamespacePrefixMapper() {
        return null;
    }
    
    /**
     * Defines the indentation string of the serialized (marshalled) document, 
     * or null to use jaxb1 default (which is 4 spaces).
     * 
     * @return null 
     */
    protected String getOutputIndentString() {
        return null;
    }
    
    /**
     * Defines the name of the encoding of the serialized (marshalled) document, 
     * or null to use jaxb1 default (which is UTF-8).
     *  
     * @return null 
     */
    protected String getOutputEncoding() {
        return null;
    }

    protected Marshaller createMarshaller(Object jaxbObject, SerializationContext context) throws JAXBException, PropertyException {
        Marshaller marshaller = getJaxbContext().createMarshaller();

        marshaller.setEventHandler(new ValidationEventCollector());
        
        if (getOutputEncoding() != null)
            marshaller.setProperty(Marshaller.JAXB_ENCODING, getOutputEncoding());
        if (isOutputFormated())
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.valueOf(isOutputFormated()));
        if (getOutputSchemaLocation() != null)
            marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, getOutputSchemaLocation());
        if (getOutputNoNamespaceSchemaLocation() != null)
            marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, getOutputNoNamespaceSchemaLocation());
        if (getOutputNamespacePrefixMapper() != null)
            marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", getOutputNamespacePrefixMapper());
        if (getOutputCharacterEscapeHandler() != null)
            marshaller.setProperty("com.sun.xml.bind.characterEscapeHandler", getOutputCharacterEscapeHandler());
        if (getOutputIndentString() != null)
            marshaller.setProperty("com.sun.xml.bind.indentString", getOutputIndentString());
        

        return marshaller;
    }

    protected Validator createValidator(Object jaxbObject, SerializationContext context) throws JAXBException, PropertyException {
        Validator validator = getJaxbContext().createValidator();
        
        validator.setEventHandler(new ValidationEventCollector());

        return validator;
    }

    protected Unmarshaller createUnmarshaller(DeserializationContext context) throws JAXBException, PropertyException {
        Unmarshaller unmarshaller = getJaxbContext().createUnmarshaller();
        
        unmarshaller.setEventHandler(new ValidationEventCollector());
        
        if (isInputValidated())
            unmarshaller.setValidating(isInputValidated());

        return unmarshaller;
    }

    protected JAXBContext getJaxbContext() {
        if (jaxbContext == null)
            jaxbContext = createJaxbContext();
        
        return jaxbContext;
    }

    protected String buildValidationErrorMessage(ValidationEventCollector errorSink) {
        if (errorSink == null || !errorSink.hasEvents())
            return "";
        
        ValidationEvent[] errors = errorSink.getEvents();
        StringBuffer sb = new StringBuffer("\nValidation Errors: \n\t");
        for(int i = 0; i < errors.length; i++) {
            appendValidationError(sb, errors[i]);
            sb.append("\n\t");
        }
        
        return sb.toString();
    }
    
    protected void appendValidationError(StringBuffer sb, ValidationEvent ev) {
        Throwable lex = ev.getLinkedException();
        
        String severity;
        switch (ev.getSeverity()) {
        case 0:
            severity = "WARN";
            break;
        case 1:
            severity = "ERROR";
            break;
        case 2:
            severity = "FATAL";
            break;

        default:
            severity = "INFO";
            break;
        }ev.getSeverity();

        int row = ev.getLocator().getLineNumber();
        int col = ev.getLocator().getColumnNumber();
        URL url = ev.getLocator().getURL();
        sb.append("\t" +
                (url != null ? url.toString() : "")
                + (row > 0 ? "[" + row + (col > 0 ? "," + col : "") + "]": "")
                + ": " + severity + ": " + ev.getMessage()
                +  (lex == null ? "" : " due to: "));
        if (lex != null)
            getAllExMsgs(sb, lex, true);
    }

    public static String getAllExMsgs(StringBuffer sb, Throwable ex, boolean includeExName) {
        if (ex == null)
            return "NO EXCEPTION!";
        String sex;
        if (includeExName)
            sex = ex.toString();
        else
            sex = ex.getMessage();
            if (sex != null)
                sb.append(sex);
            while((ex = ex.getCause()) != null)
                sb.append("\nCaused by: " + ex.toString());
            
            return sb.toString();
        }

    public static String getAllExMsgs(Throwable ex, boolean includeExName) {
    if (ex == null)
        return "NO EXCEPTION!";
    String sex;
    if (includeExName)
        sex = ex.toString();
    else
        sex = ex.getMessage();
        StringBuilder sb = new StringBuilder(sex == null? "" : sex);
        while((ex = ex.getCause()) != null)
            sb.append("\nCaused by: " + ex.toString());
        
        return sb.toString();
    }
    
    
    
    public void validateToBeSerializedObject(Object obj, SerializationContext context) throws SerializationException {
        getLogger().debug("Beginning validation.");

        ValidationEventCollector errorSink = null;
        try {
            Validator validator = createValidator(obj, context);
            errorSink = (ValidationEventCollector) validator.getEventHandler();

            validator.validateRoot(obj);
        } catch (JAXBException ex) {
            String valErrors = buildValidationErrorMessage(errorSink);
            getLogger().info("Error while validating jaxb object '{}': {}{}", 
                    new Object[] {obj, ex, valErrors});
            throw new SerializationException("Error while validating jaxb object '" + obj + "': " 
                    + ex.getMessage() + valErrors
                    , ex);
        }
    }

    public void serialize(Object obj, XMLName name, XMLOutputStream writer, SerializationContext context) throws SerializationException {
        getLogger().debug("Beginning serialisation.");

        ValidationEventCollector errorSink = null;
        try {
            if (obj != null) {
                Marshaller marshaller = createMarshaller(obj, context);
                errorSink = (ValidationEventCollector) marshaller.getEventHandler();
                
                if (isOutputValidated())
                    validateToBeSerializedObject(obj, context);
                
                marshaller.marshal(obj, new SerializerContentHandler(writer));
            } else {
                getLogger().warn("Null object passed for serialization!");
            }
        } catch (JAXBException ex) {
            String valErrors = buildValidationErrorMessage(errorSink);
            getLogger().info("Error while marshaling custom xml type '{}': {}{}", 
                    new Object[] {name, ex, valErrors});
            throw new SerializationException("Error while marshaling custom xml type '" + name + "': " 
                    + ex.getMessage() + valErrors
                    , ex);
        }
    }

    public Object deserialize(XMLName name, XMLInputStream reader, DeserializationContext context) throws DeserializationException {
        if (context.getSOAPMessage() == null)
            getLogger().debug("Beginning sample de-serialisation.");
        else
            getLogger().debug("Beginning de-serialisation.");

        if (context.getSOAPMessage() != null)
            for(Iterator it = context.getSOAPMessage().getMimeHeaders().getAllHeaders(); it.hasNext();) {
                MimeHeader mh = (MimeHeader) it.next();
                System.out.println(mh.getName()+"='"+mh.getValue()+"'");
            }
        
        ValidationEventCollector errorSink = null;
        try {
            Unmarshaller unmarshaller = createUnmarshaller(context);
            errorSink = (ValidationEventCollector) unmarshaller.getEventHandler();
    
            Object jaxbObject = unmarshaller.unmarshal(new StreamSource(new DeserializerReader(reader)));
        
            return jaxbObject;
        } catch (JAXBException ex) {
            String valErrors = buildValidationErrorMessage(errorSink);
            getLogger().info("Error while un-marshaling custom xml type '{}': {}{}", 
                    new Object[] {name, ex, valErrors});
            throw new DeserializationException("Error while un-marshaling custom xml type '" + name + "': " 
                    + ex.getMessage() + valErrors
                    , ex);
        }
    }

    public Object deserialize(XMLName name, Attribute att, DeserializationContext context) {
        getLogger().warn("Deserialize_from_Attribute invoked! Not expected.");
        // NOT USED
        return null;
    }

    
}
